Skip to content

refactor: 카카오 로그인·회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70)#71

Merged
hisonghy merged 1 commit intodevelopfrom
refactor/70
Sep 17, 2025
Merged

refactor: 카카오 로그인·회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70)#71
hisonghy merged 1 commit intodevelopfrom
refactor/70

Conversation

@hisonghy
Copy link
Contributor

@hisonghy hisonghy commented Sep 15, 2025

📌 작업 내용 및 특이사항

✅ 인증 관련 쿠키 관리

  • OAuth 가입 키, 리프레시 토큰을 쿠키로 관리하도록 개선하면서 AuthCookieHelper를 구현했습니다.
  • OAuth 가입 키, 리프레시 토큰 쿠키를 만드는 메서드 setOAuthSignupKeyCookie(), setRefreshTokenCookie()와 쿠키 이름을 기반으로 쿠키를 삭제하는 메서드cleareCookie()를 구현했습니다.
  • 쿠키는 탈취나 조작 위험을 줄이고 방지하기 위해 HttpOnly, Secure를 함께 적용하고 브라우저와 전송 경로 모두에서 보호되도록 보안을 강화했습니다.
  • 쿠키 관련 상수들을 관리하는 CookieConstants final class를 추가했습니다.

✅ 카카오 로그인 & 카카오 회원가입 로직 개선

  • 기존 카카오 로그인에서 만약 가입되지 않은 회원일 경우 예외를 던지고, 클라이언트에서 카카오 회원가입 요청 시 다시 한번 카카오 OAuth 서버로부터 인가 코드를 응답받아 사용자가 입력한 정보와 함께 요청하는 흐름이였습니다.
  • 하지만 브라우저에서 새로고침을 하게 될 경우, 저장해둔 인가 코드가 사라지는 문제가 발생해 흐름을 다음과 같이 개선했습니다.
1. 카카오 사용자 정보를 조회한 뒤 서비스 가입 여부를 판단하고, 그 결과를 `OAuthLoginOutcome` 객체로 만들어  컨트롤러에서 케이스별로 처리한다.

2. 서비스에 가입되지 않은 사용자일 경우,
카카오 OAuth에서 받아온 `KakaoUserInfo` 데이터를 `KakaoSignupProfile`로 가공하고,
이를 Redis에 `kakao::signup::profile:{UUID}` 키로 저장(TTL 15분)한다.

3. 발급한 키를 `oauth_signup_key` 라는 `HttpOnly + Secure` 쿠키에 담아 브라우저로 내려준다.(TTL 15분)
이후 사용자가 회원가입을 진행할 때 이 쿠키가 자동으로 전송되고,
서버는 쿠키의 키로 Redis에서 `KakaoSignupProfile`을 조회·검증한 뒤 가입 절차를 완료한다. 

4. 가입이 완료되면 쿠키를 만료 처리하고, Redis 키도 삭제해서 정보를 일회성으로 정리한다.

  • 위와 같이 흐름을 개선하면서, 기존엔 클라이언트와 서버 모두 로그인/회원가입에서 각각 카카오 OAuth 서버를 호출했지만 이제는 로그인 시 한 번만 호출하도록 단순화되었습니다.

  • 또한, 쿠키를 활용하기 때문에 회원가입 화면에서 새로고침이 되더라도 인가 코드가 사라지는 문제를 해결했습니다.

  • 카카오 회원가입 로직을 담당하는 KakaoSignupProfileService 클래스를 구현했습니다.

  • 카카오 가입 프로필을 Redis에 저장·관리 및 키 발급을 담당하는 KakaoSignupProfileRedisRepository와 구현체 Adapter 클래스를 구현하고,KakaoSignupProfile 모델을 구현했습니다.

  • 카카오 가입 키 관련 에러코드를 추가했습니다. (INVALID_KAKAO_SIGNUP_KEY, MISSING_KAKAO_SIGNUP_KEY)


✅ 리프레시 토큰 관리 방법 개선

  • 기존 로그인에 성공하면 AccessTokenRefreshToken을 응답 Body에 담아 응답하고, 리프레시 토큰을 클라이언트 LocalStorage에 보관하던 방식을, 보안 강화를 위해 서버가 HttpOnly + Secure 쿠키로 발급·관리하는 방식으로 개선했습니다. -> AccessToken만 응답 Body에 담아 응답
  • 리프레시 토큰 관리 방법을 개선하면서 카카오 로그인/회원가입, 토큰 재발급, 로그아웃 로직을 함께 개선했습니다.
  • 새로운 리프레시 토큰을 발급받을 경우에는 새로 쿠키에 저장하거나 기존 리프레시 쿠키에 덮어쓰고, 삭제해야할 경우에는 쿠키를 만료처리하도록 설정했습니다.

✅ 관련 테스트 코드 수정

  • AuthController 통합 테스트, KakaoLoginService 단위 테스트 코드를 개선된 로직에 맞게 수정했습니다. (쿠키 적용)
  • KakaoSignupProfileService 단위 테스트를 추가했습니다.
  • MemberService의 소셜 정보로 멤버를 조회하는 로직의 반환 값을 Optinal<Member>로 수정하면서 테스트 코드도 함께 수정했습니다.

✅ 기타 사항

  • AuthFacade의 카카오 로그인·회원가입, 토큰 재발급 흐름에서 Application 계층의 반환값과 Presentation 계층의 응답 값을 분리했습니다.
    로그인은 가입 여부에 따라 다르게 분기 처리가 필요하기 때문에 Application에서는 결과를 표현하는 OAuthLoginOutcome DTO를 두어 반환하고,
    기존 토큰 정보를 담은 TokenResponseTokenInfo로 바꿔 Application 계층의 DTO로 두고, 실제 클라이언트 응답은 역할과 의미가 명확하도록 LoginResponse, ReissueTokenResponse Presentation DTO로 새로 구성했습니다.
  • 향후 새로운 소셜 로그인이 추가될 수도 있음을 생각해 공통으로 쓰일 수 있는 항목(예: OAuth 가입 쿠키명, 로그인 결과 DTO(OAuthLoginOutcome))는 OAuth prefix를 사용해 명명했습니다.
  • global.common.constants의 상수 클래스들을 enum class -> final class로 변경했습니다.
    CookieConstants에서 쿠키 이름과 TTL을 함께 관리하는데, 두 필드의 타입이 달라 enum으로 관리하면 모든 상수에 두 필드를 강제로 두어야 하고, 사용하지 않는 필드는 null로 채워야합니다.
    사용하지 않는 필드를 null 값으로 대체하는 방법보다 다양한 타입을 안전하게 다룰 수 있는 final class 기반 상수 클래스로 전환하고, UrlConstants, CacheConstanstsfinal class로 변경해 일관되도록 했습니다.

🌱 관련 이슈


🔍 참고사항(선택)

[브라우저 쿠키 자동 전송 테스트(크롬 기준)]

  • 로그인 api 요청으로 브라우저 Cookieauth_refresh 쿠키가 저장되어 있는 상태입니다.
  • 이후 토큰 재발급 api를 요청해 브라우저 쿠키 전송 테스트를 진행했습니다.

HTTP 요청 시
스크린샷 2025-09-16 오후 4 52 02
스크린샷 2025-09-16 오후 4 52 22

  • Request Header에 발급받은 auth_refresh 쿠키가 없음을 확인할 수 있습니다.
  • Request Header에 리프레시 쿠키가 없어 에러가 발생한 모습입니다.

HTTPS 요청 시
스크린샷 2025-09-16 오후 4 38 33
스크린샷 2025-09-16 오후 5 01 42

  • Request Header.Cookieauth_refresh=<발급받은 리프레시 토큰>이 있는 걸 확인할 수 있습니다.

  • 추가로 Postman/Talend API Tester 등 API 테스트 툴은 브라우저 보안 정책을 그대로 적용하지 않아 HTTP에서도 Secure 여부와 무관하게 쿠키가 전송되는 것처럼 동작했습니다.
  • 그래서 최종 검증을 크롬 개발자 도구로 진행했는데,
    크롬은 localhost/127.0.0.1를 안전한 컨텍스트로 특례 취급해 HTTP에서도 Secure 쿠키가 전송되는 현상이 있었습니다.
  • 이에 로컬 hostsapi.studytrip.local같은 새로운 호스트를 추가해 테스트 도메인을 분리했고, HTTP에서는 Secure 쿠키가 전송되지 않고 HTTPS에서만 전송됨을 확인했습니다.
  • 로컬에서 쿠키를 검증할 때는 API 테스트 도구 대신 브라우저(DevTools) 를 사용하고, 브라우저 정책 차이에 따라 필요 시 호스트명을 별도 도메인으로 변경해 테스트해야 한다는 점을 참고하면 좋을 것 같습니다.

📚 기타(선택)

@hisonghy hisonghy self-assigned this Sep 15, 2025
@hisonghy hisonghy added ✨feature 구현, 개선 사항 관련 부분 🪄refactor 기능 개선 및 리팩토링 labels Sep 15, 2025
@hisonghy hisonghy changed the title refactor: 카카오 로그인, 카카오 회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70) refactor: 카카오 로그인/회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70) Sep 15, 2025
@hisonghy hisonghy changed the title refactor: 카카오 로그인/회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70) refactor: 카카오 로그인·회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70) Sep 15, 2025
Copy link
Contributor

@chaiminwoo0223 chaiminwoo0223 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고 많으셨습니다. 보안을 세심히 고려하신 점이 인상 깊었습니다. 몇가지 궁금한 내용이 있어서 코드 리뷰를 남겼습니다. 확인 부탁드려요~^^

public record TokenInfo(String accessToken, String refreshToken, long refreshTokenExpiresIn) {
public static TokenInfo of(
String accessToken, String refreshToken, long refreshTokenExpiresIn) {
return new TokenInfo(accessToken, refreshToken, refreshTokenExpiresIn);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refreshTokenExpiresIn(리프레시토큰 만료시간)을 DTO에 같이 담아서 전달하고 있네요. 같이 전달한 이유가 궁금합니다.

Copy link
Contributor Author

@hisonghy hisonghy Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

쿠키 관리는 HTTP와 관련된 로직이므로 Presentation 계층에서 전담하도록 설정해두었습니다.
리프레시 토큰과 쿠키의 만료시간을 동일하게 관리하기 위해 Application 계층의 TokenInfo DTO에서 발급된 리프레시 토큰의 만료시간을 함께 반환하고,
Presentation 계층에서 리프레시 쿠키를 생성할 때 토큰의 실제 만료시간을 기준으로 쿠키의 생명주기를 정확하게 관리하고자 refreshTokenExpiresIn(리프레시토큰 만료시간)을 함께 반환하도록 구성했습니다.

private final String value;
public static final String AUTH_REISSUE_TOKEN_PREFIX = "auth::reissue::token:";
public static final String AUTH_LOGOUT_TOKEN_PREFIX = "auth::logout::token:";
public static final String OAUTH_SIGNUP_PROFILE_PREFIX = "%s::signup::profile:";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%s::signup::profile:에서 %s는 어떻게 사용이 되는지 궁금합니다.

Copy link
Contributor Author

@hisonghy hisonghy Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다양한 OAuth 가입 프로필을 저장할 때 OAuth Provider별 고유한 키로 관리하고자 %s 네임스페이스 플레이스홀더를 사용했습니다.

이 방식을 사용하면 kakao::signup::profile, google::signup::profile 같은 제공자별 상수를 별도로 관리하지 않고, formatted() 메서드로 런타임에 동적으로 상황에 맞는 키를 생성할 수 있습니다.

%s에는 kakao, google, naver 같은 OAuth 제공자 식별자가 들어가도록 설정했고, 최종적인 키의 형태는 <provider>::signup::profile:<signupKey> 형태로 키가 저장됩니다.

문자열은 +StringBuilder로 이어 붙일 수 있지만, 가독성 측면에서 %s포맷을 우선 선택했습니다.
다만 %s/formatted()는 내부적으로 매 호출마다 Formatter 객체 생성, 포맷 문자열 파싱, Object 배열 할당 작업이 일어나 +/StringBuilder보다 확실히 무겁고 성능적으로 떨어집니다.
일반적인 트래픽에서는 체감이 미미하다고는 하나 대량/대규모 처리 시에는 체감이 크게 날 수도 있습니다.
가독성을 위해 %s포맷을 사용하는 방법과 성능적인 부분을 챙겨가는 방법 중에 어떤 방법이 더 좋을까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

운영 환경을 고려했을 때 성능적인 부분도 분명 중요하지만, 현재 방식으로도 충분히 안정적으로 동작할 수 있을 것 같아요. 지금 방식을 유지해도 괜찮을 것 같아요.

memberRepository.findBySocialProviderAndSocialId(socialProvider, socialId);

MemberPolicy.validateNotDeleted(member);
if (member.isEmpty()) return Optional.empty();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if(member.isEmpty) ...도 다른 검증 로직처럼 MemebrPolicy에 두면, 재사용성과 유지보수 측면에서 더 좋을 것 같아요.

Copy link
Contributor Author

@hisonghy hisonghy Sep 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (member.isEmpty()) ... 검증을 MemberPolicy에 두면 분기 처리하는데 있어 좀 애매해질 수 있을 것 같습니다.

Optional<Member>가 비어 있을 경우 예외로 처리를 해야하는 상황이라면 MemberPolicy로 두는게 적절해보이지만, 지금 같은 경우는 예외 없이 케이스별 다른 분기를 가져야합니다.

MemberPolicyboolean validateIsEmpty(Optional<Member> member) 메서드를 생성해 검증을 할 수 있지만, 결국 MemberService에서 다시 if (MemberPolicy.validateIsEmpty(member)) return Optional.empty()처럼 다시 분기 처리를 해줘야하는 상황이 발생할 수 있습니다.

대신 if (member.isEmpty())MemberPolicy.validateNotDeleted() 로직을 하나의 Optional 흐름으로 합쳐 map으로 처리하도록 수정할 수 있을 것 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확인했습니다. 그럼 MemberService에 if(member.isEmpty) ...를 그대로 유지하는게 좋겠네요.

Copy link
Contributor

@chaiminwoo0223 chaiminwoo0223 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다. 커밋 내용 병합 후 머지 부탁드립니다!

* feat: 인증 관련 쿠키를 관리하는 AuthCookieHelper 구현
* feat: 쿠키 관련 상수를 관리하는 CookieConstants final class 추가

* refactor: 카카오 로그인 로직 리팩토링
* refactor: 카카오 회원가입 로직 리팩토링
* refactor: 토큰 재발급, 로그아웃 로직 리팩토링

* chore: global.common.constants 상수 클래스를 enum -> final class로 변경

* refactor: /.well-known 정적 리소스 경로 추가

* test: KakaoSignupProfileService 단위 테스트 추가
* test: AuthController 통합 테스트 코드 리팩토링
* test: 카카오 로그인, 회원가입, 로그아웃 Fixture 클래스 리팩토링경
* test: KakaoLoginService 단위 테스트 코드 리팩토링
* test: MemberService 단위 테스트 코드 리팩토링
@hisonghy hisonghy merged commit ac5a4d1 into develop Sep 17, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature 구현, 개선 사항 관련 부분 🪄refactor 기능 개선 및 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🪄[REFACTOR]: 카카오 로그인, 카카오 회원가입 리팩토링

2 participants